import os
import random

import chardet
import time
import docx
import re
import json
import uuid
import threading
from typing import List, Dict, Tuple
from flask import Flask, request, jsonify, make_response
from openai import OpenAI
from collections import Counter
from flask_cors import CORS

app = Flask(__name__)
CORS(app, supports_credentials=True, origins=["*"])


class ExamBotManager:
    def __init__(self):
        self.lock = threading.Lock()
        self.bots = {}

    def get_bot(self, session_id):
        with self.lock:
            return self.bots.get(session_id)

    def create_bot(self, session_id, config):
        with self.lock:
            bot = SmartExamBot(
                api_key=config["api_key"],
                base_url=config["base_url"],
                max_questions=config["max_questions"]
            )
            self.bots[session_id] = bot
            return bot


exam_bot_manager = ExamBotManager()


class SmartExamBot:
    def __init__(self, api_key: str, base_url: str, max_questions: int = 5):
        self.api_key = api_key
        self.base_url = base_url
        self.client = OpenAI(api_key=api_key, base_url=base_url)
        self.max_follow_ups = 3
        self.question_count = 0
        self.total_score = 0
        self.max_possible_score = 0
        self.max_questions = max_questions
        self.current_follow_ups = 0
        self.question_scores = []

        # 正则表达式模式
        self.cannot_answer_patterns = re.compile(
            r'不会|不知道|不清楚|没学过|不懂|不了解|不会做|不知道怎么答|无法回答|我举不出来例子|分析不出来',
            re.IGNORECASE
        )
        self.irrelevant_patterns = re.compile(
            r'跑题|偏题|无关|答非所问|换个题|另一个|不相关|说点别的|其他内容|别的话题',
            re.IGNORECASE
        )
        self.image_patterns = re.compile(
            r'如图|图示|图片|图表|图[.\d]+|figure|graph|chart|diagram|illustration',
            re.IGNORECASE
        )

        # 数据存储
        self.knowledge_content = ""
        self.keywords = []
        self.current_question = ""
        self.question_dimensions = []
        self.messages = []
        self.required_core_concepts = []  # 新增：存储题目要求覆盖的核心概念

        # 扩展无关话题检测
        self.unrelated_topics = {
            '天气', '吃饭', '电影', '游戏', '睡觉', '时间', '日期',
            '音乐', '歌曲', '明星', '电视剧', '综艺', '体育', '运动'
        }

    def _extract_keywords(self, text: str) -> List[str]:
        """从知识库中提取关键词"""
        # 提取高频名词术语（中文2-6个字）
        words = re.findall(r'\b[\u4e00-\u9fa5]{2,6}\b', text)
        word_counts = Counter(words)

        # 过滤掉无意义的常见词
        stop_words = {'可以', '一个', '这个', '我们', '进行', '需要', '应该', '问题', '方法'}
        keywords = [word for word, count in word_counts.most_common(20)
                    if word not in stop_words and count > 1]

        return keywords[:10] if keywords else ["数据", "分析", "技术"]  # 默认关键词

    def _extract_dimensions(self, question: str) -> List[str]:
        """精确提取题目要求的维度"""
        match = re.search(r'请从(.*?)分析', question)
        if match:
            dimensions_text = match.group(1)
            return [dim.strip() for dim in re.split(r'[、,，和与及]', dimensions_text) if dim.strip()]
        return []

    def _calculate_answer_quality(self, user_answer: str) -> Tuple[bool, List[str]]:
        """计算答案质量"""
        # 1. 检查是否直接复制题目
        if user_answer.strip() == self.current_question.strip():
            return (False, ["答案直接复制了题目内容"])

        # 2. 检查维度覆盖
        covered_dimensions = []
        for dim in self.question_dimensions:
            if dim in user_answer and len(user_answer.split(dim)[1]) > 10:  # 确保有实际内容
                covered_dimensions.append(dim)

        # 3. 检查知识库关键词
        knowledge_keywords = [kw for kw in self.keywords if kw in user_answer]

        # 4. 检查实际内容长度
        content_length = len(re.sub(r'\s+', '', user_answer))

        return (len(covered_dimensions) > 0, covered_dimensions, knowledge_keywords, content_length)

    def detect_encoding(self, file_path: str) -> str:
        if file_path.lower().endswith('.docx'):
            return 'docx'
        try:
            with open(file_path, 'rb') as file:
                return chardet.detect(file.read(10240))['encoding'] or 'utf-8'
        except Exception as e:
            raise Exception(f"编码检测失败: {str(e)}")

    def read_file_content(self, file_path: str) -> str:
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"文件不存在: {file_path}")

        if not os.path.isfile(file_path):
            raise IsADirectoryError(f"{file_path}不是一个文件")

        try:
            if file_path.lower().endswith('.docx'):
                doc = docx.Document(file_path)
                return '\n'.join(para.text for para in doc.paragraphs)

            encoding = self.detect_encoding(file_path)
            for enc in [encoding, 'utf-8', 'gbk', 'latin-1']:
                try:
                    with open(file_path, 'r', encoding=enc, errors='replace') as file:
                        return file.read()
                except UnicodeDecodeError:
                    continue
            raise Exception("无法识别文件编码")
        except Exception as e:
            raise Exception(f"文件读取失败: {str(e)}")

    def get_ai_response(self, messages: List[Dict], max_retries: int = 3) -> str:
        retry_count = 0
        while retry_count < max_retries:
            try:
                response = self.client.chat.completions.create(
                    model="deepseek-v3",
                    messages=messages,
                    temperature=0.2,
                    timeout=30
                )
                return response.choices[0].message.content
            except Exception as e:
                retry_count += 1
                if retry_count >= max_retries:
                    raise Exception(f"API请求失败: {str(e)}")
                time.sleep(2)

    def is_cannot_answer(self, user_input: str) -> bool:
        user_input = user_input.strip()
        if not user_input or re.fullmatch(r'[\s,.!?;]+', user_input):
            return True
        return self.cannot_answer_patterns.search(user_input) is not None

    def is_irrelevant(self, user_answer: str) -> Tuple[bool, str]:
        user_answer = user_answer.strip().lower()

        # 1. 直接匹配无关表述
        if self.irrelevant_patterns.search(user_answer):
            return True, "回答明确表示与题目无关"

        # 2. 检查无关主题
        for topic in self.unrelated_topics:
            if topic in user_answer:
                return True, f"回答涉及无关主题({topic})"

        # 3. 强制校验“核心概念覆盖”
        if self.required_core_concepts:
            missing_concepts = [c for c in self.required_core_concepts if c.lower() not in user_answer]
            if missing_concepts:
                return True, f"未覆盖核心概念：{', '.join(missing_concepts)}"

        # 4. 分层校验关键词（核心概念 > 题目维度 > 通用关键词）
        all_keywords = (
                self.required_core_concepts +  # 第一层：核心概念
                self.question_dimensions +  # 第二层：题目要求的分析维度
                self.keywords  # 第三层：知识库提取的通用关键词
        )

        # 需至少覆盖核心概念/维度中的1个 + 通用关键词中的1个
        has_core_or_dim = any(c in user_answer for c in self.required_core_concepts + self.question_dimensions)
        has_general_kw = any(kw in user_answer for kw in self.keywords)

        if not (has_core_or_dim and has_general_kw):
            return True, "回答未关联核心概念、分析维度或知识库关键词"

        return False, ""

    def init_exam(self, knowledge_content: str):
        self.knowledge_content = knowledge_content
        self.keywords = self._extract_keywords(knowledge_content)
        self.messages = [
            {"role": "system", "content": self._get_system_prompt()},
            {"role": "assistant", "content": "考试准备就绪，请输入'开始'进行第一题。"}
        ]

    def _get_system_prompt(self):
        return f"""
        你是一个公平的智能考试机器人，需要基于以下知识库内容出题和评分：
        {self.knowledge_content[:2000]}...

        评分标准：
        1. 维度覆盖（40%）：检查是否覆盖题目要求的分析维度
        2. 知识关联（30%）：是否引用知识库中的关键概念和技术
        3. 内容深度（20%）：分析的深度和详细程度
        4. 实际案例（10%）：是否提供实际应用案例

        评分格式：分数|具体反馈|是否追问(是/否)
        """

    def generate_question(self):
        if self.question_count >= self.max_questions:
            return None

        self.question_count += 1
        self.current_follow_ups = 0

        for retry in range(5):  # 最多重试5次
            prompt = f"""基于以下知识库内容出题：
            {self.knowledge_content[:3000]}

            要求：
            1. 题目必须要求从2-3个维度分析
            2. 必须包含知识库中的核心概念
            3. 不能包含图片相关表述
            4. 题目格式：[题目内容] 核心概念覆盖：[概念1、概念2...]
            5. 只需返回题目完整内容（含核心概念覆盖部分）"""

            self.messages.append({"role": "user", "content": prompt})
            question = self.get_ai_response(self.messages)

            # 解析题目和核心概念
            full_question_pattern = re.compile(r'^(.*?)核心概念覆盖：(.*?)$', re.DOTALL)
            match = full_question_pattern.search(question)

            if match:
                self.current_question = match.group(1).strip()
                self.required_core_concepts = [c.strip() for c in match.group(2).split('、') if c.strip()]
            else:
                # 未匹配到核心概念，使用备用解析
                self.current_question = question
                self.required_core_concepts = self._extract_keywords(self.current_question)[:3]

            # 验证题目质量
            self.question_dimensions = self._extract_dimensions(self.current_question)
            if (len(self.question_dimensions) >= 2 and
                    not self.image_patterns.search(self.current_question) and
                    len(self.current_question) > 10):
                self.messages.append({"role": "assistant", "content": self.current_question})
                self.max_possible_score += 10
                return self.current_question

            self.messages.pop()  # 移除无效尝试

        # 生成备用题目
        fallback_questions = [
            "请从原理、技术和应用三个维度分析数据采集技术 核心概念覆盖：数据采集、传感器、物联网",
            "请比较网络爬虫和API接口两种数据获取方式的优缺点 核心概念覆盖：网络爬虫、API接口、数据获取",
            "请说明数据清洗的主要步骤和常用工具 核心概念覆盖：数据清洗、缺失值处理、异常值检测"
        ]

        question = random.choice(fallback_questions)
        match = full_question_pattern.search(question)

        if match:
            self.current_question = match.group(1).strip()
            self.required_core_concepts = [c.strip() for c in match.group(2).split('、') if c.strip()]
        else:
            self.current_question = question
            self.required_core_concepts = self._extract_keywords(self.current_question)[:3]

        self.question_dimensions = self._extract_dimensions(self.current_question)
        return self.current_question

    def evaluate_answer(self, user_answer: str):
        # 1. 基础检查
        if self.is_cannot_answer(user_answer):
            return self._handle_invalid_answer(0, "检测到无法回答的表述")

        if self.is_irrelevant(user_answer)[0]:
            return self._handle_invalid_answer(0, "检测到无关回答")

        # 2. 检查是否复制题目
        if user_answer.strip() == self.current_question.strip():
            return self._handle_invalid_answer(0, "答案直接复制了题目内容")

        # 3. 校验是否回答了题目核心问题
        core_question_match = re.search(r'请从(.*?)分析', self.current_question)
        if core_question_match:
            required_dims = core_question_match.group(1).lower()
            if required_dims not in user_answer.lower():
                return self._handle_invalid_answer(0, f"未从「{required_dims}」维度分析，回答偏离核心问题")

        # 4. 计算答案质量
        is_valid, covered_dims, knowledge_kws, content_len = self._calculate_answer_quality(user_answer)

        if not is_valid or content_len < 20:
            return self._handle_invalid_answer(0, "回答未包含有效内容")

        # 5. 调用AI评分（带质量检查）
        self.messages.append({"role": "user", "content": user_answer})

        evaluation_prompt = f"""请基于以下标准评分：
        题目：{self.current_question}
        知识库关键词：{', '.join(self.keywords[:5])}
        要求维度：{', '.join(self.question_dimensions)}
        核心概念：{', '.join(self.required_core_concepts)}

        评分标准：
        - 维度覆盖（{', '.join(covered_dims)}/{len(self.question_dimensions)}）
        - 知识关联（{len(knowledge_kws)}个关键词）
        - 内容长度：{content_len}字

        按格式返回：分数|反馈|是否追问"""

        evaluation = self.get_ai_response(self.messages + [
            {"role": "system", "content": evaluation_prompt}
        ])

        # 6. 解析评分结果
        try:
            score_part, feedback, need_follow_up = evaluation.strip().split('|')
            score = max(0, min(10, float(score_part)))

            # 确保分数与内容质量匹配
            if content_len < 50 and score > 5:
                score = 5
                feedback = "内容过短，调整分数。" + feedback

            self.total_score += score
            self.question_scores.append(score)

            return {
                "score": score,
                "feedback": feedback,
                "need_follow_up": need_follow_up.strip().lower() == '是',
                "covered_dimensions": covered_dims
            }
        except:
            return self._handle_invalid_answer(5, "评分解析错误，按中等分数处理")

    def _handle_invalid_answer(self, score: float, reason: str):
        self.total_score += score
        self.question_scores.append(score)
        return {
            "score": score,
            "feedback": reason,
            "need_follow_up": False
        }

    def ask_follow_up(self):
        if self.current_follow_ups >= self.max_follow_ups:
            return None

        self.current_follow_ups += 1

        follow_up = self.get_ai_response(self.messages + [
            {"role": "system", "content": f"""请针对以下薄弱点提出追问：
            已覆盖维度：{', '.join(self.question_dimensions)}
            知识库关键词：{', '.join(self.keywords[:3])}
            只需返回问题内容"""}
        ])

        # 清理问题格式
        follow_up = re.sub(r'^追问[:：]\s*', '', follow_up.strip())
        return follow_up if len(follow_up) > 5 else "请补充更多细节"


# Flask路由保持不变（与原始代码相同）
@app.route('/')
def index():
    with open('test.html', 'r', encoding='utf-8') as f:
        return f.read()


@app.route('/start_exam', methods=['POST'])
def start_exam():
    session_id = request.cookies.get('session_id') or str(uuid.uuid4())
    CONFIG = {
        "api_key": "sk-43f6c93de4ff47ba9cbd7e0dc9ed68b7",
        "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
        "file_path": r"E:/第3章 数据获取.docx",
        "max_questions": 5
    }

    bot = exam_bot_manager.get_bot(session_id) or exam_bot_manager.create_bot(session_id, CONFIG)

    try:
        knowledge_content = bot.read_file_content(CONFIG["file_path"])
        bot.init_exam(knowledge_content)
        question = bot.generate_question()

        response = jsonify({
            'question': question,
            'question_count': bot.question_count,
            'max_questions': bot.max_questions
        })
        response.set_cookie('session_id', session_id, httponly=True, samesite='None', secure=True)
        return response
    except Exception as e:
        return jsonify({'error': str(e)}), 500


@app.route('/submit_answer', methods=['POST'])
def submit_answer():
    session_id = request.cookies.get('session_id')
    if not session_id:
        return jsonify({'error': '缺少会话ID'}), 400

    bot = exam_bot_manager.get_bot(session_id)
    if not bot:
        return jsonify({'error': '考试未开始或会话已过期'}), 400

    result = bot.evaluate_answer(request.json.get('answer', ''))

    if result['need_follow_up']:
        follow_up = bot.ask_follow_up()
        if follow_up:
            result['follow_up_question'] = follow_up

    return jsonify(result)


@app.route('/next_question', methods=['GET'])
def next_question():
    session_id = request.cookies.get('session_id')
    bot = exam_bot_manager.get_bot(session_id)
    if not bot:
        return jsonify({'error': '无效会话'}), 400

    if bot.question_count >= bot.max_questions:
        return jsonify({'message': '考试已完成'})

    question = bot.generate_question()
    return jsonify({'question': question}) if question else jsonify({'error': '生成题目失败'}), 500


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)